package unifor.mia.redes.relprotocol;

import java.util.ArrayDeque;
import java.util.ArrayList;

import unifor.mia.redes.relprotocol.channel.Channel;
import unifor.mia.redes.relprotocol.msg.ACK;
import unifor.mia.redes.relprotocol.msg.Pacote;

public class ReliableSender extends ReliableProtocolComponent {

   private int TAMANHO_JANELA = 5; // tamanho da janela 
   private ArrayDeque<Pacote> pacotesNaoReconhecidos = new ArrayDeque<Pacote>();
   private ArrayList<Pacote> filaPacotes;
   private int timeout = 500;
   private long time = 0l;
   private boolean timerOn = false;
   private String mensagem = "";

   private CommunicationMonitor monitor;
           
   public String getMensagem() {
      return mensagem;
   }

   public void setMensagem(String mensagem) {
      this.mensagem = mensagem;
      filaPacotes = empacota(getMensagem());
   }

   public void setCommunicationMonitor(CommunicationMonitor cm) {
      this.monitor = cm;
   }

   @Override
   public void run() {

      if(monitor == null){
         throw new RuntimeException("Monitor de comunicação não definido");
      }
         
      send(getMensagem());

   }

   public ArrayList<Pacote> getPacotesEmpacotados() {
      return filaPacotes;
   }

   public void send(String texto) {
      if (texto == null) {
         texto = "";
      }

      ACK confirmation = null;

      int i = 0;
      while (i < filaPacotes.size() || !pacotesNaoReconhecidos.isEmpty()) {
//			System.out.println("[Sender] Tamanho do buffer de pacotes nao reconhecidos = " + pacotesNaoReconhecidos.size());
         if (i < filaPacotes.size()) {
            Pacote pckt = filaPacotes.get(i);
            if (pacotesNaoReconhecidos.size() < TAMANHO_JANELA) {
               Channel.write(pckt);
               if (pacotesNaoReconhecidos.isEmpty()) {
                  startTimer();
               }
               pacotesNaoReconhecidos.add(pckt);
               monitor.packetSent(pckt);
               i++;
//				} else {
//					System.out.println("[Sender] Janela cheia. Aguardando um confirmação do destinatario...");
            }
            if (i > filaPacotes.size()) {
               System.out.println("[Sender] Envio de mensagens finalizado. Falta receber " + pacotesNaoReconhecidos.size() + " ACKs");
            }
         }
//			System.out.println("[Sender] Tamanho do buffer de pacotes nao reconhecidos = " + pacotesNaoReconhecidos.size());
         confirmation = Channel.getConfirmation();
         if (confirmation != null && confirmation.getMessage().equals("ACK")) {
            System.out.println("[Sender] " + confirmation.getMessage() + " com SEQ = " + confirmation.getSeq() + " recebido no Sender");
            confirmar(confirmation.getSeq());
//			} else if(confirmation != null) {
//				System.out.println("[Sender] " + confirmation.getMessage() + " com SEQ = " + confirmation.getSeq() + " recebido no Sender e DESCONSIDERADO");
         }
//			System.out.println("[Sender] Tamanho do buffer de pacotes nao reconhecidos = " + pacotesNaoReconhecidos.size());
         checkTimeout();

      }
      filaPacotes.clear();
   }

   private void printMensagensNaoConfirmadas() {
      Object[] pckts = pacotesNaoReconhecidos.toArray();

      String txt = "";
      for (int i = 0; i < pckts.length; i++) {
         txt = txt + ((Pacote) pckts[i]).getSeq() + ", ";
      }
      System.out.println("Mensagens ainda sem confirmação: " + txt);
   }

   private void startTimer() {
      timerOn = true;
      time = System.currentTimeMillis();
   }

   private void stopTimer() {
      timerOn = false;
   }

   private void checkTimeout() {
      long now = System.currentTimeMillis();
      if (timerOn && (now - time) >= timeout) {
         System.out.println("[Sender] TIMEOUT! Reenviando " + pacotesNaoReconhecidos.size() + " pacotes nao reconhecidos até agora");
         printMensagensNaoConfirmadas();
         timeout();
      }
   }

   private void timeout() {
      Object[] msgs = pacotesNaoReconhecidos.toArray();

      for (int i = 0; i < msgs.length; i++) {
         System.out.println("[Sender] Enviando backlog");
         monitor.packetReSent((Pacote) msgs[i]);
         Channel.write((Pacote) msgs[i]);
      }
      startTimer();
   }

   private void confirmar(long sequencial) {
      if (sequencial >= 0) {
         Pacote m = pacotesNaoReconhecidos.peek();
         while (m != null && m.getSeq() <= sequencial) {
            m = pacotesNaoReconhecidos.poll();
            monitor.packetAcknowledged(m);
            m = pacotesNaoReconhecidos.peek();
         }
         if (pacotesNaoReconhecidos.isEmpty()) {
            stopTimer();
         } else {
            startTimer();
         }
      }
   }

   public ArrayList<Pacote> empacota(String mensagem) {

      ArrayList<Pacote> bloco = new ArrayList<Pacote>();

      int j = 1;
      int ultInd = 0;

      if (mensagem == null || mensagem.equals("")) {
         bloco.add(criaMensagem(""));
      } else {
         for (int i = 0; i < mensagem.length(); i++) {
            if (j == TAMANHO_PACOTE || i == mensagem.length() - 1) {
               Pacote msg = criaMensagem(mensagem.substring(ultInd, i + 1));
               bloco.add(msg);
               ultInd = i + 1;
               j = 1;
            } else {
               j++;
            }
         }
      }
      bloco.get(0).setStarting();
      bloco.get(bloco.size() - 1).setEnding();
      return bloco;
   }

}
